// ==UserScript==
// @name         chatgpt Flexible Fetch Data Handler with Logging
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Log requests and responses on all websites with detailed information.
// @match        https://chat.openai.com/*
// @grant        none
// @require      https://cdn.jsdelivr.net/npm/lodash/lodash.min.js
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    window.requestsResponses = {};
    const enableLogging = true; // Flag to enable or disable logging

    function logInfo(title, info, titleColor, contentColor) {
        if (!enableLogging) return;
        console.groupCollapsed(title, titleColor, 'color: white;', contentColor, 'color: white;');
        if (typeof info === 'object' && info !== null) {
            Object.entries(info).forEach(([key, value]) => {
                if (['ResponseBody', 'RequestBody'].includes(key) && typeof value === 'string') {
                    try {
                        const parsed = JSON.parse(value);
                        console.log(`${key}:`);
                        console.dir(parsed);
                    } catch {
                        console.log(`${key}:`, value);
                    }
                } else if (typeof value === 'object') {
                    console.log(`${key}:`);
                    console.dir(value);
                } else {
                    console.log(`${key}:`, value);
                }
            });
        } else {
            console.log('Info:', info);
        }
        console.groupEnd();
    }


// Function to process and log response
function processAndLogResponse(response, url, method, config) {
    const contentType = response.headers.get('content-type');
    let promise;

    if (contentType?.includes('text/event-stream')) {
        // Both 'text/event-stream' and other Blob types are processed using processBlobResponse
        promise = processBlobResponse(response);
    } else if (contentType?.includes('json')) {
        promise = response.json();
    } else if (contentType?.includes('text')) {
        promise = response.text()
    } else {
        promise = Promise.resolve('Unkonwn response content-type');
    }


    promise.then(data => {
        const hasResponseBody = typeof data === 'string' ? data.trim() !== '' : true;
        const hasResponseBodyContent = hasResponseBody ? 'Yes' : 'No';
        const hasResponseBodyColor = hasResponseBody ? 'color: green;' : 'color: red;';
        const responseColor = 'color: yellow;';
        const Response = "Response";
        const responseTitle = `Fetch %c${Response}%c \n\t- ${method} ${url} \n\t- Has Body: %c${hasResponseBodyContent}%c`;

        logInfo(responseTitle, {
            ResponseHeaders: [...response.headers].reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
            ResponseBody: data
        }, responseColor, hasResponseBodyColor);
        window.requestsResponses[url] = { request: config, response: { status: response.status, headers: [...response.headers], body: data }};
    }).catch(error => {
        console.error(`Error processing response for ${url}:`, error);
        window.requestsResponses[url] = { request: config, response: { error: error.toString() }};
    });
}


function processBlobResponse(response) {
    return new Promise((resolve, reject) => {
        const contentType = response.headers.get('content-type');
        const charsetMatch = contentType.match(/charset=([a-zA-Z0-9-]+)/);
        const charset = charsetMatch ? charsetMatch[1] : 'utf-8'; // Default to UTF-8
        const decoder = new TextDecoder(charset);
        const reader = response.body.getReader();
        let mergedData = {}; // Object to store merged data

        // Function to handle the received data chunk
        const onDataReceived = async (event) => {
            const chunk = await event.data;
            const textData = decoder.decode(chunk);
            const jsonDictionaries = textData.split('\n');

            for (let jsonStr of jsonDictionaries) {
                // Remove "data:" prefix and "[DONE]" marker, then trim
                jsonStr = jsonStr.replace(/^data:\s*/, '').replace(/\[DONE\]/, '').trim();

                if (jsonStr) {
                    try {
                        const jsonData = JSON.parse(jsonStr);
                        Object.assign(mergedData, jsonData);
                    } catch (error) {
                        console.error('Error parsing JSON data:', error);
                    }
                }
            }
        };

        // Function to read and process each chunk
        reader.read().then(function processChunk(result) {
            if (result.done) {
                if (Object.keys(mergedData).length > 0) {
                    resolve(mergedData); // Resolve with the merged data if it's not empty
                } else {
                    resolve(null); // Resolve with null if mergedData is empty
                }
                return;
            }

            onDataReceived({ data: result.value }); // Trigger onDataReceived for this chunk
            return reader.read().then(processChunk);
        }).catch(error => {
            console.error('Error reading Blob data:', error);
            if (Object.keys(mergedData).length > 0) {
                resolve(mergedData); // Resolve with the merged data if it's not empty
            } else {
                reject(error);
            }
        });
    });
}



function handlePartialData(response, error) {
    if (error.name === 'AbortError') {
        console.warn('Fetch aborted. Attempting to handle partial data.');
        // Try to read as text for partial data
        return response.text().then(text => text.substring(0, 1000));  // Limit to first 1000 chars for brevity
    }
    throw error;  // Re-throw other errors
}


    // Override XMLHttpRequest
    const originalXHROpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function(method, url) {
        this._method = method;
        this._url = url;
        this.addEventListener('load', function() {
            const title = `%cXMLHttpRequest%c - ${method} ${url} - Has Content: %c${this.responseText ? 'Yes' : 'No'}%c`;
            logInfo(title, {
                ResponseHeaders: this.getAllResponseHeaders(),
                ResponseBody: this.responseText
            }, 'color: blue;', this.responseText ? 'color: green;' : 'color: red;');
            window.requestsResponses[url] = { request: { method, headers: {} }, response: { status: this.status, headers: this.getAllResponseHeaders(), body: this.responseText }};
        });
        originalXHROpen.apply(this, arguments);
    };

    // Override Fetch
    const originalFetch = window.fetch;
    window.fetch = function(url, config = {}) {
        const method = config.method || 'GET';
        const requestTitle = `%cFetch Request%c - ${method} ${url} - Has Body: %c${config.body ? 'Yes' : 'No'}%c`;
        logInfo(requestTitle, {
            RequestHeaders: config.headers || {},
            RequestBody: config.body || 'No Body'
        }, 'color: green;', config.body ? 'color: green;' : 'color: red;');

        return originalFetch.apply(this, arguments).then(response => {
          const responseClone = response.clone();
            processAndLogResponse(responseClone, url, method, config);

            return response;
        }).catch(error => {
            console.error(`Fetch request failed for ${url}:`, error);
            window.requestsResponses[url] = { request: config, response: { error: error.toString() }};
        });
    };


function generateUUID() {
    let d = new Date().getTime();
    if (typeof performance !== 'undefined' && typeof performance.now === 'function'){
        d += performance.now(); // 使用高精度时间
    }
    return 'xxxxxxxx-xxxx-xxxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
        const r = (d + Math.random() * 16) % 16 | 0;
        d = Math.floor(d / 16);
        return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
    });
}


    // Method to resend a stored request
    /**
     * Send an updated request based on stored configuration.
     * @param {string} url - The URL to send the request to.
     * @param {Object|null} updateHeaders - Headers to be updated.
     * @param {Object} updates - Object containing paths and values to update in the request body.
     */
      window.sendRequest = function(url, updateHeaders = null, updates = {}) {
          const storedConfig = window.requestsResponses[url] ? window.requestsResponses[url].request : {};
          console.log('storedConfig:', storedConfig);

          // 初始化 Headers 对象
          let headers = new Headers(storedConfig.headers || {});

          // 更新 Headers 对象
          if (updateHeaders) {
              console.log(`Updating headers for ${url}`);
              Object.entries(updateHeaders).forEach(([key, value]) => {
                  console.log(`Adding header ${key}: ${value}`);
                  headers.set(key, value);
              });
          }

          console.log('Current headers:', Array.from(headers.entries()));

          // 准备请求配置
          const config = {
              ...storedConfig,
              headers: headers, // 使用更新后的 Headers 对象
              method: storedConfig.method || 'POST'
          };

          // 更新请求体（如果需要）
          if (storedConfig.body && Object.keys(updates).length > 0) {
              console.log(`Updating body for ${url}`);
              const existingBody = JSON.parse(storedConfig.body);

              Object.entries(updates).forEach(([path, value]) => {
                  _.set(existingBody, path, value);
              });

              config.body = JSON.stringify(existingBody);
          }

          console.log(`Sending request to ${url} with config:`, config);
          console.log('config headers:', Array.from(config.headers.entries()));
          return fetch(url, config)
            .then(response => {
              console.log(`typeof response: `, typeof response);
              // 检查响应是否存在并且是一个有效的对象
              if (!response || typeof response !== 'object') {
                throw new Error('Invalid response');
              }

              // 检查响应的状态码
              if (!response.ok) {
                throw new Error(`Request failed with status ${response.status}`);
              }

              // 解析响应的内容类型
              const contentType = response.headers.get('content-type');

              // 根据内容类型处理响应
              if (contentType && contentType.includes('application/json')) {
                // 如果响应是 JSON 数据
                return response.json();
              } else if (contentType && contentType.includes('text/plain')) {
                // 如果响应是纯文本
                return response.text();
              } else {
                // 其他情况，例如二进制数据
                return response.blob();
              }
            })
            .catch(error => {
              // 处理请求失败或其他错误
              console.error(`Error while sending request to ${url}:`, error);
              throw error; // 将错误继续传递
            });
      };



})();
